package de.jonasjelten.jcalc.math;

import de.jonasjelten.jcalc.exception.*;
import java.math.BigDecimal;
import java.math.BigInteger;

/**
 *
 * @author michael
 */
public class JNumber implements Comparable<JNumber> {
    // this variable saves the internal value of the JNumber variable.

//old version    private final BigDecimal realValue, imaginaryValue;
    public final BigInteger realNumerator, imaginaryNumerator, denominator;

    // constructors.
    public JNumber(BigInteger realNumerator, BigInteger imaginaryNumerator, BigInteger denominator) throws JMathematicalException {
        switch (denominator.compareTo(BigInteger.ZERO)) {
            case 0:
                throw new JDivisionByZeroException();
            case -1:
                realNumerator = realNumerator.negate();
                imaginaryNumerator = imaginaryNumerator.negate();
                denominator = denominator.negate();
            default:
                if (denominator.compareTo(BigInteger.valueOf(1)) != 0 || (!imaginaryNumerator.equals(BigInteger.ZERO) && !realNumerator.equals(BigInteger.ZERO))) {
                    try {
                        //0llen wegstreichen:
                        //while(beideunddenominator.haveTrailingZero) then beide nullen weg
                        
                        //Wirkliches kürzen: // Evtl Stellenlimit?
                        System.out.println("Starting to find JNumber shortener...");
                        BigInteger dualnumberggt = BigInteger.ZERO;

                        dualnumberggt = JMathOps.ggT(imaginaryNumerator, realNumerator);
                        
                        BigInteger shortenFactor = JMathOps.ggT(dualnumberggt, denominator);
                        if (shortenFactor.compareTo(BigInteger.valueOf(1)) != 0) {
                            System.out.println("Shortening with " + shortenFactor.toString() + ".");
                            realNumerator = realNumerator.divide(shortenFactor);
                            imaginaryNumerator = imaginaryNumerator.divide(shortenFactor);
                            denominator = denominator.divide(shortenFactor);
                        }
                    } catch (JGCDException e) {
                        System.out.println("Error in shortening JNumber. Error: "+e.toString());
                    }
                }
                this.realNumerator = realNumerator;
                this.imaginaryNumerator = imaginaryNumerator;
                this.denominator = denominator;

        }
    }

    public JNumber(int realNumerator, int imaginaryNumerator, int denominator) throws JMathematicalException {
        this(BigInteger.valueOf(realNumerator), BigInteger.valueOf(imaginaryNumerator), BigInteger.valueOf(denominator));
    }

    public JNumber(int value) throws JMathematicalException {
        this(value, 0, 1);
    }

    public JNumber(BigDecimal realValue, BigDecimal imaginaryValue) throws JMathematicalException {
        this(
                realValue.movePointRight(JMathOps.scaleRest(realValue, imaginaryValue)[0]).unscaledValue(),
                imaginaryValue.movePointRight(JMathOps.scaleRest(realValue, imaginaryValue)[1]).unscaledValue(),
                BigInteger.valueOf(10).pow(JMathOps.scaleRest(realValue, imaginaryValue)[2]));
    }

    public JNumber(BigInteger realValue, BigInteger imaginaryValue) throws JMathematicalException {
        this(realValue, imaginaryValue, BigInteger.ONE);
    }

    public JNumber(BigInteger realValue) throws JMathematicalException {
        this(realValue, BigInteger.ZERO);
    }

    
    public JNumber(BigDecimal realValue) throws JMathematicalException {
        this(realValue, BigDecimal.ZERO);
    }

    
    public JNumber(BigDecimal value, boolean real) throws JMathematicalException {
        this(real ? value : BigDecimal.ZERO, real ? BigDecimal.ZERO : value);
    }

    public JNumber(BigInteger value, boolean real) throws JMathematicalException {
        this(real ? value : BigInteger.ZERO, real ? BigInteger.ZERO : value);
    }

    public JNumber(String realValue) throws JMathematicalException {
        this(realValue, true);
    }

    public JNumber(String value, boolean real) throws JMathematicalException {
        this(real ? value : "0", real ? "0" : value);
    }

    public JNumber(String realValue, String imaginaryValue) throws JMathematicalException {
        this(new BigDecimal(realValue), new BigDecimal(imaginaryValue));
    }

    // GET methods
    // this method returns the real value of the JNumber variable.
    public JNumber getRealValue() throws JMathematicalException {
        return new JNumber(realNumerator, BigInteger.ZERO, denominator);
    }

    // this method returns the imaginary value of the JNumber variable.
    public JNumber getImaginaryValue() throws JMathematicalException {
        return new JNumber(imaginaryNumerator, BigInteger.ZERO, denominator);
    }

    public JNumber stripRealValue() throws JMathematicalException {
        return new JNumber(BigInteger.ZERO, imaginaryNumerator, denominator);
    }

    // methods that will return boolean
    // this method returns true if the imaginary value is non-zero.
    public boolean hasComplexPart() {
        return (imaginaryNumerator.compareTo(BigInteger.ZERO) != 0);
    }

    // delete at some point.
    @Deprecated
    public boolean isComplex() {
        return hasComplexPart();
    }

    public boolean hasRealPart() {
        return (realNumerator.compareTo(BigInteger.ZERO) != 0);
    }

    // this method returns true if the number is zero.
    public boolean isZero() {
        return ((realNumerator.compareTo(BigInteger.ZERO)) == 0 && (imaginaryNumerator.compareTo(BigInteger.ZERO) == 0));
    }

    public boolean isRealValuePositive() {
        return ((realNumerator.compareTo(BigInteger.ZERO)) < 0);
    }

    public boolean isFraction() {
        return denominator.compareTo(BigInteger.ONE) > 0;
    }

    // this method returns true if the number is a real integer.
    public boolean isRealInteger() throws JMathematicalException {
        return (!getRealValue().isFraction()
                && (imaginaryNumerator.compareTo(BigInteger.ZERO) == 0));
    }

    // this method returns true if the number is a non-negative real integer.
    public boolean isNonNegativeRealInteger() throws JMathematicalException {
        return (isRealInteger() && (realNumerator.compareTo(BigInteger.ZERO) >= 0));
    }

    // this method returns true if the number is a positive real integer.
    public boolean isPositiveRealInteger() throws JMathematicalException {
        return (isRealInteger() && (realNumerator.compareTo(BigInteger.ZERO) > 0));
    }

    // this method returns true if the number's real and imaginary part ar both integers.
    public boolean isInteger() {
        return (!isFraction());
    }


    // toString method.
    //TODO: ENUM zum auswaehlen
    @Override
    public String toString() {
        if (realNumerator == null || imaginaryNumerator == null || denominator == null) {
            return "[NOT INITED]";
        } else if (!hasComplexPart()) { //Nur real
            return "[" + realNumerator.toString() + (denominator.compareTo(BigInteger.ONE) != 0 ? "/" + denominator : "") + "]";
        } else if (!hasRealPart()) { //Nur komplex
            return "[" + imaginaryNumerator.toString() + "i" + (denominator.compareTo(BigInteger.ONE) != 0 ? "/" + denominator : "") + "]";
        } else { // Beides
            return "[" + realNumerator.toString() + " + " + imaginaryNumerator.toString() + "i" + (denominator.compareTo(BigInteger.ONE) != 0 ? "/" + denominator : "") + "]";
        }
        /*
        if (!hasComplexPart()) {
        return new BigDecimal(realNumerator).divide(new BigDecimal(denominator), JPreferences.nachkommastellen, RoundingMode.HALF_UP).toString();
        } else {
        return new BigDecimal(realNumerator).divide(new BigDecimal(denominator), JPreferences.nachkommastellen, RoundingMode.HALF_UP).toString() + " + "
        + new BigDecimal(realNumerator).divide(new BigDecimal(denominator), JPreferences.nachkommastellen, RoundingMode.HALF_UP).toString() + "i";
        }*/
    }

    // zero-argument calculation methods.
    // negation method.
    public JNumber negate() throws JMathematicalException {
        return new JNumber(realNumerator.negate(), imaginaryNumerator.negate(), denominator);
    }

    // conjugation method. negates the number's imaginary part.
    public JNumber conjungate() throws JMathematicalException {
        return new JNumber(realNumerator, imaginaryNumerator.negate(), denominator);
    }

    // faculty method.
    public JNumber faculty() throws JMathematicalException {
        if (isNonNegativeRealInteger()) {
            int numberOfSubThreads = JPreferences.facultythreadcount;
            BigInteger value = realNumerator;

            JNumberFacultyHelperThread[] facs = new JNumberFacultyHelperThread[numberOfSubThreads];

            BigInteger[] partition = new BigInteger[numberOfSubThreads + 1];

            partition[0] = BigInteger.ONE;
            for (int i = 1; i < numberOfSubThreads; i++) {
                partition[i] = value.multiply(BigInteger.valueOf(i)).divide(BigInteger.valueOf(numberOfSubThreads)).add(BigInteger.ONE);
            }
            partition[numberOfSubThreads] = value.add(BigInteger.ONE);

            for (int i = 0; i < numberOfSubThreads; i++) {
                facs[i] = new JNumberFacultyHelperThread(partition[i], partition[i + 1].subtract(BigInteger.ONE));
                facs[i].start();
            }

            BigInteger result = BigInteger.ONE;
            for (int i = 0; i < numberOfSubThreads; i++) {
                result = result.multiply(facs[i].getResult());
            }

            return new JNumber(result);
        } else {
            System.out.println("Failed the Faculty of " + this.toString());
            throw new JMathematicalException("Error while calculating faculty of " + this.toString() + ": Not a non-negative real integer.");
        }
    }



    
    public JNumber facultyBounded(JNumber limit) throws JMathematicalException {
        

        if (isNonNegativeRealInteger() && limit.isNonNegativeRealInteger()) {
            
            
            /*int numberOfSubThreads = JPreferences.facultythreadcount;
            BigInteger value = realNumerator;

            JNumberFacultyHelperThread[] facs = new JNumberFacultyHelperThread[numberOfSubThreads];

            BigInteger[] partition = new BigInteger[numberOfSubThreads + 1];

            partition[0] = BigInteger.ONE;
            for (int i = 1; i < numberOfSubThreads; i++) {
                partition[i] = value.multiply(BigInteger.valueOf(i)).divide(BigInteger.valueOf(numberOfSubThreads)).add(BigInteger.ONE);
            }
            partition[numberOfSubThreads] = value.add(BigInteger.ONE);

            for (int i = 0; i < numberOfSubThreads; i++) {
                facs[i] = new JNumberFacultyHelperThread(partition[i], partition[i + 1].subtract(BigInteger.ONE));

                facs[i].start();
            }*/
            JNumber lowlim, highlim;
            // wenn eins this is größer sonst anerer operator ist größer
            int selfisbigger = this.realNumerator.compareTo(limit.realNumerator);

            if(selfisbigger == 0) { // beide gleich -> keine muls nötig
                return this;
            } else if(selfisbigger < 0) {
                lowlim = this;
                highlim = limit;
            } else {
                lowlim = limit;
                highlim = this;
            }

            JNumberFacultyHelperThread evalth = new JNumberFacultyHelperThread(lowlim.realNumerator, highlim.realNumerator);

            evalth.start();

            BigInteger result = evalth.getResult();
            
            return new JNumber(result);
        } else {
            System.out.println("Failed the Faculty of " + this.toString());
            throw new JMathematicalException("Error while calculating faculty of " + this.toString() + ": Not a non-negative real integer.");
        }
    }

    // exp method.
    public JNumber exp() {
        //am besten wohl mit taylor-reihe lösbar.
        return null;
    }

    // natural logarithm method.
    public JNumber log() {
        //kp wie lösbar.
        return null;
    }

    // trig fkt.
    public JNumber sin() {
        //am besten wohl lösbar, indem die exp-fkt genutzt wird. genauso bei allen anderen trig. fkt.
        return null;
    }

    // argumented math ops.
    // there is one method for every argumentedmathoptype/numbertype pair.
    public JNumber sum(JNumber arg) throws JMathematicalException {
        BigInteger newDenominator = JMathOps.kgV(this.denominator, arg.denominator);
        BigInteger thisErweiterung = newDenominator.divide(this.denominator);
        BigInteger argErweiterung = newDenominator.divide(arg.denominator);

        return new JNumber(
                this.realNumerator.multiply(thisErweiterung).add(arg.realNumerator.multiply(argErweiterung)),
                this.imaginaryNumerator.multiply(thisErweiterung).add(arg.imaginaryNumerator.multiply(argErweiterung)),
                newDenominator);
    }

    public JNumber mul(JNumber arg) throws JMathematicalException {
        return new JNumber(
                realNumerator.multiply(arg.realNumerator).subtract(imaginaryNumerator.multiply(arg.imaginaryNumerator)),
                realNumerator.multiply(arg.imaginaryNumerator).add(imaginaryNumerator.multiply(arg.realNumerator)),
                denominator.multiply(arg.denominator));
        //TODO: Multithreaded multiply
    }

    /**
     * complex number division.
     *
     * @param arg
     * @return
     * @throws JMathematicalException
     */
    public JNumber div(JNumber arg) throws JMathematicalException {
        return new JNumber(
                arg.denominator.multiply(realNumerator.multiply(arg.realNumerator).add(imaginaryNumerator.multiply(arg.imaginaryNumerator))),
                arg.denominator.multiply(imaginaryNumerator.multiply(arg.realNumerator).subtract(realNumerator.multiply(arg.imaginaryNumerator))),
                denominator.multiply(arg.realNumerator.multiply(arg.realNumerator).add(arg.imaginaryNumerator.multiply(arg.imaginaryNumerator))));
    }

    public JNumber mod(JNumber arg) throws JMathematicalException {
        if (arg.isZero()) {
            throw new JDivisionByZeroException();
        }
        if (!(isInteger() && arg.isInteger())) {
            throw new JMathematicalException("Modulo operation is only allowed for integer arguments.");
        }
        if (isComplex() || arg.isComplex()) {
            //TODO: komplexes modulo implementieren.
            throw new JMathematicalException("Modulo operation is not allowed for complex arguments.");
        }

        return new JNumber(realNumerator.mod(arg.realNumerator));
    }

    public JNumber pow(JNumber arg) throws JMathematicalException {
        if ((isZero()) && (arg.isZero())) {
            throw new JMathematicalException("Unable to evaluate the expression 0^0.");
        }
        if (!arg.isRealInteger()) {
            //TODO: komplexe power.

            throw new JMathematicalException("Power operation is not allowed for complex exponent.");
        }

        if (arg.realNumerator.compareTo(BigInteger.ZERO) < 0) {
            return new JNumber(BigInteger.ONE).div(pow(arg.negate()));
        }

        try {
            JNumber result = new JNumber(BigInteger.ZERO);

            for (BigInteger exp = arg.realNumerator; exp.compareTo(BigInteger.ZERO) > 0; exp = exp.subtract(BigInteger.ONE)) {
                result = mul(result);
            }

            return result;

        } catch (ArithmeticException e) {
            throw new JMathematicalException(e.toString());
        }
    }

    @Override
    public int compareTo(JNumber o) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public int compareReal(JNumber comparator) {
        BigInteger obenself = comparator.realNumerator.multiply(this.denominator);
        BigInteger obenother = this.realNumerator.multiply(comparator.denominator);

        return obenself.compareTo(obenother);
    }

    public int compareImaginary(JNumber comparator) {
        BigInteger obenself = comparator.imaginaryNumerator.multiply(this.denominator);
        BigInteger obenother = this.imaginaryNumerator.multiply(comparator.denominator);

        return obenself.compareTo(obenother);
    }
    
}
